"""
C++ code emitter for generating header and source files from IR.

This module provides the CppEmitter class which orchestrates code generation
from IR nodes to C++ files.
"""

from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import Any

import yaml
from jinja2 import Environment, FileSystemLoader

from orchestrator.ir.nodes import (
    ClassDeclaration,
    ConstructorDeclaration,
    FieldDeclaration,
    MethodDeclaration,
    PropertyDeclaration,
)
from orchestrator.mapping.symbol_resolver import SymbolResolver
from orchestrator.mapping.type_mapper import TypeMapper

from .formatter import CodeFormatter
from .naming import NamingConvention


class CppEmitter:
    """
    Emits C++ code from IR nodes.

    Handles header and source file generation with proper formatting,
    includes, and namespace management.
    """

    def __init__(
        self,
        type_mapper: TypeMapper | None = None,
        symbol_resolver: SymbolResolver | None = None,
        config_path: str | Path | None = None,
    ):
        """
        Initialize CppEmitter.

        Args:
            type_mapper: TypeMapper instance (creates default if None)
            symbol_resolver: SymbolResolver instance (creates default if None)
            config_path: Path to codegen-style.yaml (uses default if None)
        """
        self.type_mapper = type_mapper or TypeMapper()
        self.symbol_resolver = symbol_resolver or SymbolResolver()

        # Setup default namespace mappings
        self.symbol_resolver.setup_default_mappings()

        # Load configuration
        if config_path is None:
            default_path = (
                Path(__file__).parent.parent.parent.parent / "config" / "codegen-style.yaml"
            )
            config_path = default_path

        self.config_path = Path(config_path)
        self.config: dict[str, Any] = {}
        self._load_config()

        # Initialize naming convention and formatter based on config
        naming_config = self.config.get("naming", {})
        self.naming = NamingConvention(
            class_style=naming_config.get("class_style", "PascalCase"),
            method_style=naming_config.get("method_style", "snake_case"),
            field_style=naming_config.get("field_style", "snake_case"),
            parameter_style=naming_config.get("parameter_style", "snake_case"),
            private_prefix=naming_config.get("private_prefix", "m_"),
            constant_style=naming_config.get("constant_style", "UPPER_CASE"),
        )

        indent_config = self.config.get("indentation", {})
        format_config = self.config.get("formatting", {})
        self.formatter = CodeFormatter(
            indent_style=indent_config.get("style", "spaces"),
            indent_width=indent_config.get("width", 4),
            max_line_length=format_config.get("max_line_length", 100),
        )

        # Setup Jinja2 environment
        template_dir = Path(__file__).parent / "templates"
        self.jinja_env = Environment(
            loader=FileSystemLoader(str(template_dir)),
            trim_blocks=True,
            lstrip_blocks=True,
        )

    def _load_config(self) -> None:
        """Load code generation configuration"""
        if not self.config_path.exists():
            raise FileNotFoundError(f"Codegen config not found: {self.config_path}")

        with open(self.config_path, "r") as f:
            self.config = yaml.safe_load(f)

    def emit_class(
        self, class_node: ClassDeclaration, output_dir: Path
    ) -> tuple[Path, Path]:
        """
        Emit header and source files for a class.

        Args:
            class_node: IR ClassDeclaration node
            output_dir: Output directory for generated files

        Returns:
            Tuple of (header_path, source_path)
        """
        # Convert namespace
        cpp_namespace = self.symbol_resolver.resolve_namespace(class_node.namespace)
        namespace_parts = self.symbol_resolver.get_cpp_namespace_parts(cpp_namespace)

        # Convert class name
        cpp_class_name = self.naming.convert_class_name(class_node.name)

        # Generate header file
        header_content = self._generate_header(class_node, cpp_class_name, namespace_parts)

        # Generate source file
        source_content = self._generate_source(class_node, cpp_class_name, namespace_parts)

        # Write files
        output_dir.mkdir(parents=True, exist_ok=True)

        header_ext = self.config.get("files", {}).get("header_extension", ".hpp")
        source_ext = self.config.get("files", {}).get("source_extension", ".cpp")

        header_filename = f"{self.naming.to_snake_case(cpp_class_name)}{header_ext}"
        source_filename = f"{self.naming.to_snake_case(cpp_class_name)}{source_ext}"

        header_path = output_dir / header_filename
        source_path = output_dir / source_filename

        header_path.write_text(header_content)
        source_path.write_text(source_content)

        return header_path, source_path

    def _generate_header(
        self, class_node: ClassDeclaration, cpp_class_name: str, namespace_parts: list[str]
    ) -> str:
        """Generate header file content"""
        template = self.jinja_env.get_template("class_header.hpp.jinja")

        # Collect all types used in the class for includes
        all_types = self._collect_types(class_node)
        includes = self._generate_includes(all_types, is_header=True)

        # Generate header guard
        cpp_namespace_str = "::".join(namespace_parts) if namespace_parts else ""
        header_guard = self.naming.get_header_guard(cpp_namespace_str, cpp_class_name)

        # Generate file header comment
        file_header = self._generate_file_header(
            f"{self.naming.to_snake_case(cpp_class_name)}.hpp",
            f"Header file for {cpp_class_name} class",
        )

        # Generate class comment
        class_comment = self._generate_class_comment(class_node)

        # Convert base classes
        base_classes = [
            f"public {self._convert_type(base)}" for base in class_node.base_types
        ]

        # Generate constructors
        constructors = self._generate_constructors(class_node, cpp_class_name)

        # Generate destructor
        destructor = self._generate_destructor(class_node, cpp_class_name)

        # Categorize methods and fields by access level
        public_methods = []
        protected_methods = []
        private_methods = []

        for method in class_node.methods:
            method_data = self._generate_method_signature(method)
            if "public" in method.modifiers:
                public_methods.append(method_data)
            elif "protected" in method.modifiers:
                protected_methods.append(method_data)
            else:
                private_methods.append(method_data)

        # Handle properties as methods (getter/setter)
        for prop in class_node.properties:
            prop_methods = self._generate_property_methods(prop)
            if "public" in prop.modifiers:
                public_methods.extend(prop_methods)
            elif "protected" in prop.modifiers:
                protected_methods.extend(prop_methods)
            else:
                private_methods.extend(prop_methods)

        # Categorize fields
        public_fields = []
        protected_fields = []
        private_fields = []

        for field in class_node.fields:
            field_data = self._generate_field_declaration(field)
            if "public" in field.modifiers:
                public_fields.append(field_data)
            elif "protected" in field.modifiers:
                protected_fields.append(field_data)
            else:
                private_fields.append(field_data)

        # Render template
        use_pragma_once = (
            self.config.get("include_guards", {}).get("style", "pragma_once") == "pragma_once"
        )

        return template.render(
            file_header=file_header,
            use_pragma_once=use_pragma_once,
            header_guard=header_guard,
            includes=includes,
            namespace_parts=namespace_parts,
            class_name=cpp_class_name,
            class_comment=class_comment,
            base_classes=base_classes,
            constructors=constructors,
            destructor=destructor,
            public_methods=public_methods,
            protected_methods=protected_methods,
            private_methods=private_methods,
            public_fields=public_fields,
            protected_fields=protected_fields,
            private_fields=private_fields,
        )

    def _generate_source(
        self, class_node: ClassDeclaration, cpp_class_name: str, namespace_parts: list[str]
    ) -> str:
        """Generate source file content"""
        template = self.jinja_env.get_template("class_source.cpp.jinja")

        # Generate includes (header file + any additional needed)
        header_include = self.naming.get_include_name(
            "::".join(namespace_parts) if namespace_parts else "", cpp_class_name
        )

        includes_list = [self.formatter.format_include(header_include, system=False)]
        includes_list.append(self.formatter.format_include("stdexcept", system=True))

        includes = "\n".join(includes_list)

        # Generate file header
        file_header = self._generate_file_header(
            f"{self.naming.to_snake_case(cpp_class_name)}.cpp",
            f"Implementation file for {cpp_class_name} class",
        )

        # Generate constructor implementations
        constructors = self._generate_constructor_implementations(
            class_node, cpp_class_name, namespace_parts
        )

        # Generate destructor implementation
        destructor = self._generate_destructor_implementation(
            class_node, cpp_class_name, namespace_parts
        )

        # Generate method implementations
        methods = self._generate_method_implementations(
            class_node, cpp_class_name, namespace_parts
        )

        return template.render(
            file_header=file_header,
            includes=includes,
            namespace_parts=namespace_parts,
            constructors=constructors,
            destructor=destructor,
            methods=methods,
        )

    def _collect_types(self, class_node: ClassDeclaration) -> list[str]:
        """Collect all types used in a class"""
        types = []

        # Base types
        types.extend(class_node.base_types)

        # Field types
        for field in class_node.fields:
            types.append(field.field_type)

        # Property types
        for prop in class_node.properties:
            types.append(prop.property_type)

        # Method return types and parameters
        for method in class_node.methods:
            types.append(method.return_type)
            for param in method.parameters:
                types.append(param.param_type)

        # Constructor parameters
        for ctor in class_node.constructors:
            for param in ctor.parameters:
                types.append(param.param_type)

        return types

    def _generate_includes(self, types: list[str], is_header: bool = True) -> str:
        """Generate include directives for types"""
        system_includes: set[str] = set()
        user_includes: set[str] = set()

        for type_name in types:
            if type_name in ("void", ""):
                continue

            result = self.type_mapper.map_type(type_name)
            for include in result.includes:
                # System includes start with <
                if include.startswith("<") and include.endswith(">"):
                    system_includes.add(include[1:-1])
                else:
                    system_includes.add(include)

        # Format includes
        lines = []
        for inc in sorted(system_includes):
            lines.append(self.formatter.format_include(inc, system=True))

        if user_includes:
            lines.append("")
            for inc in sorted(user_includes):
                lines.append(self.formatter.format_include(inc, system=False))

        return "\n".join(lines) if lines else ""

    def _convert_type(self, csharp_type: str) -> str:
        """Convert C# type to C++ type"""
        result = self.type_mapper.map_type(csharp_type)
        return result.cpp_type

    def _generate_file_header(self, filename: str, description: str) -> str:
        """Generate file header comment"""
        if not self.config.get("comments", {}).get("generate_file_headers", True):
            return ""

        comment_style = self.config.get("comments", {}).get("style", "doxygen")
        lines = [
            f"@file {filename}",
            f"@brief {description}",
            f"@date {datetime.now().strftime('%Y-%m-%d')}",
            "",
            "Generated by cpp-orchestrator",
        ]

        if comment_style == "block":
            result = ["/**"]
            result.extend(f" * {line}" if line else " *" for line in lines)
            result.append(" */")
            return "\n".join(result)
        else:
            return "\n".join(f"/// {line}" if line else "///" for line in lines)

    def _generate_class_comment(self, class_node: ClassDeclaration) -> str:
        """Generate class documentation comment"""
        if not self.config.get("comments", {}).get("generate_method_docs", True):
            return ""

        comment = f"@brief {class_node.name} class"

        comment_style = self.config.get("comments", {}).get("style", "doxygen")

        if comment_style == "block":
            return f"/** {comment} */"
        else:
            return f"/// {comment}"

    def _generate_constructors(
        self, class_node: ClassDeclaration, cpp_class_name: str
    ) -> list[dict[str, Any]]:
        """Generate constructor signatures for header"""
        constructors = []

        # Generate default constructor if configured
        if self.config.get("constructors", {}).get("generate_default", True) and not class_node.constructors:
            sig = f"{cpp_class_name}()"
            if self.config.get("constructors", {}).get("use_default_keyword", True):
                sig += " = default"

            constructors.append(
                {"signature": sig, "comment": self.formatter.format_comment("Default constructor", "doxygen")}
            )

        # Generate constructors from IR
        for ctor in class_node.constructors:
            params = []
            for param in ctor.parameters:
                cpp_type = self._convert_type(param.param_type)
                cpp_name = self.naming.convert_parameter_name(param.name)
                params.append(f"{cpp_type} {cpp_name}")

            signature = f"{cpp_class_name}({', '.join(params)})"

            comment = self.formatter.format_comment(f"Constructor", "doxygen")

            constructors.append({"signature": signature, "comment": comment})

        return constructors

    def _generate_destructor(
        self, class_node: ClassDeclaration, cpp_class_name: str
    ) -> dict[str, Any] | None:
        """Generate destructor signature for header"""
        is_virtual = (
            class_node.base_types
            or self.config.get("methods", {}).get("virtual_destructors", True)
        )

        sig = ""
        if is_virtual:
            sig += "virtual "
        sig += f"~{cpp_class_name}()"

        if self.config.get("constructors", {}).get("use_default_keyword", True):
            sig += " = default"

        comment = self.formatter.format_comment("Destructor", "doxygen")

        return {"signature": sig, "comment": comment}

    def _generate_method_signature(self, method: MethodDeclaration) -> dict[str, Any]:
        """Generate method signature for header"""
        # Convert return type
        cpp_return_type = self._convert_type(method.return_type)

        # Convert method name
        cpp_method_name = self.naming.convert_method_name(method.name)

        # Convert parameters
        params = []
        for param in method.parameters:
            cpp_type = self._convert_type(param.param_type)
            cpp_name = self.naming.convert_parameter_name(param.name)
            params.append(f"{cpp_type} {cpp_name}")

        # Build signature
        signature = f"{cpp_return_type} {cpp_method_name}({', '.join(params)})"

        # Add qualifiers
        qualifiers = []
        if self.config.get("methods", {}).get("const_correctness", True):
            # Simple heuristic: methods starting with "Get" or "Is" are const
            if method.name.startswith(("Get", "Is", "Has")):
                qualifiers.append("const")

        if method.is_virtual:
            signature = "virtual " + signature

        if qualifiers:
            signature += " " + " ".join(qualifiers)

        if method.is_override and self.config.get("methods", {}).get("override_keyword", True):
            signature += " override"

        if method.is_abstract:
            signature += " = 0"

        # Generate comment
        comment = self.formatter.format_comment(f"{method.name} method", "doxygen")

        return {"signature": signature, "comment": comment, "name": method.name}

    def _generate_property_methods(
        self, prop: PropertyDeclaration
    ) -> list[dict[str, Any]]:
        """Generate getter/setter methods for a property"""
        methods = []
        cpp_type = self._convert_type(prop.property_type)
        cpp_prop_name = self.naming.convert_method_name(prop.name)

        # Generate getter
        if prop.has_getter:
            getter_name = f"get_{cpp_prop_name}"
            getter_sig = f"{cpp_type} {getter_name}() const"
            methods.append(
                {
                    "signature": getter_sig,
                    "comment": self.formatter.format_comment(f"Get {prop.name}", "doxygen"),
                    "name": getter_name,
                }
            )

        # Generate setter
        if prop.has_setter:
            setter_name = f"set_{cpp_prop_name}"
            setter_sig = f"void {setter_name}({cpp_type} value)"
            methods.append(
                {
                    "signature": setter_sig,
                    "comment": self.formatter.format_comment(f"Set {prop.name}", "doxygen"),
                    "name": setter_name,
                }
            )

        return methods

    def _generate_field_declaration(self, field: FieldDeclaration) -> dict[str, Any]:
        """Generate field declaration"""
        cpp_type = self._convert_type(field.field_type)
        is_private = "private" in field.modifiers or "public" not in field.modifiers
        cpp_name = self.naming.convert_field_name(field.name, is_private=is_private)

        if field.initializer:
            declaration = f"{cpp_type} {cpp_name} = {field.initializer}"
        else:
            declaration = f"{cpp_type} {cpp_name}"

        comment = self.formatter.format_comment(f"{field.name} field", "doxygen")

        return {"declaration": declaration, "comment": comment}

    def _generate_constructor_implementations(
        self, class_node: ClassDeclaration, cpp_class_name: str, namespace_parts: list[str]
    ) -> list[dict[str, Any]]:
        """Generate constructor implementations for source file"""
        constructors = []

        # Only generate implementations for non-default constructors
        for ctor in class_node.constructors:
            params = []
            for param in ctor.parameters:
                cpp_type = self._convert_type(param.param_type)
                cpp_name = self.naming.convert_parameter_name(param.name)
                params.append(f"{cpp_type} {cpp_name}")

            # Use qualified name for constructor
            qualified_name = f"{cpp_class_name}::{cpp_class_name}"
            signature = f"{qualified_name}({', '.join(params)})"

            body = ctor.body if ctor.body else None

            constructors.append({"signature": signature, "body": body})

        return constructors

    def _generate_destructor_implementation(
        self, class_node: ClassDeclaration, cpp_class_name: str, namespace_parts: list[str]
    ) -> dict[str, Any] | None:
        """Generate destructor implementation for source file"""
        # Only generate if not using = default
        if self.config.get("constructors", {}).get("use_default_keyword", True):
            return None

        qualified_name = f"{cpp_class_name}::~{cpp_class_name}"
        signature = f"{qualified_name}()"

        return {"signature": signature, "body": None}

    def _generate_method_implementations(
        self, class_node: ClassDeclaration, cpp_class_name: str, namespace_parts: list[str]
    ) -> list[dict[str, Any]]:
        """Generate method implementations for source file"""
        methods = []

        for method in class_node.methods:
            # Skip abstract methods
            if method.is_abstract:
                continue

            cpp_return_type = self._convert_type(method.return_type)
            cpp_method_name = self.naming.convert_method_name(method.name)

            params = []
            for param in method.parameters:
                cpp_type = self._convert_type(param.param_type)
                cpp_name = self.naming.convert_parameter_name(param.name)
                params.append(f"{cpp_type} {cpp_name}")

            # Use qualified name
            qualified_name = f"{cpp_class_name}::{cpp_method_name}"
            signature = f"{cpp_return_type} {qualified_name}({', '.join(params)})"

            # Add const qualifier if needed
            if self.config.get("methods", {}).get("const_correctness", True):
                if method.name.startswith(("Get", "Is", "Has")):
                    signature += " const"

            body = method.body if method.body else None

            methods.append({"signature": signature, "body": body, "name": cpp_method_name})

        # Generate property method implementations
        for prop in class_node.properties:
            prop_methods = self._generate_property_method_implementations(
                prop, cpp_class_name
            )
            methods.extend(prop_methods)

        return methods

    def _generate_property_method_implementations(
        self, prop: PropertyDeclaration, cpp_class_name: str
    ) -> list[dict[str, Any]]:
        """Generate property getter/setter implementations"""
        methods = []
        cpp_type = self._convert_type(prop.property_type)
        cpp_prop_name = self.naming.convert_method_name(prop.name)

        # Backing field name
        backing_field = f"m_{cpp_prop_name}"

        # Getter
        if prop.has_getter:
            getter_name = f"get_{cpp_prop_name}"
            qualified_name = f"{cpp_class_name}::{getter_name}"
            signature = f"{cpp_type} {qualified_name}() const"

            body = prop.getter_body if prop.getter_body else f"    return {backing_field};"

            methods.append({"signature": signature, "body": body, "name": getter_name})

        # Setter
        if prop.has_setter:
            setter_name = f"set_{cpp_prop_name}"
            qualified_name = f"{cpp_class_name}::{setter_name}"
            signature = f"void {qualified_name}({cpp_type} value)"

            body = prop.setter_body if prop.setter_body else f"    {backing_field} = value;"

            methods.append({"signature": signature, "body": body, "name": setter_name})

        return methods
